/**********************************************************
 *
 * Function:
 *
 * Makes command line option handling for C++ programs easy.
 *
 * modification:
 * 
 * add 'addUsage(std::string line)' method for help information.
 * add 'printUsage()' method for help information.
 *
 * help:
 * see the document at github.com/Optionhandler
 *
 * shoujian zhang, 2020, wuhan university
 *
 **********************************************************/

#ifndef OPTION_HANDLER_H
#define OPTION_HANDLER_H

// Dependencies
#include <map>
#include <string>
#include <vector>
#include <iostream>
#include <stdexcept>

/**********************************************************
 *
 * @class: OptionHandler
 * @description: Parses input and determines whether options
 * and arguments to those options are set.
 *
 **********************************************************/


namespace gpstk
{

    class no_argument_for_required : public std::invalid_argument
    {
    public:
        no_argument_for_required(std::string option_name) :
                std::invalid_argument("requiredArg option '" + option_name + "' without argument")
        {}
    };

    class argument_for_none : public std::invalid_argument
    {
    public:
        argument_for_none(std::string option_name) :
                std::invalid_argument("noArg option '" + option_name + "' with argument")
        {}
    };


    // 定义Option
    struct Option
    {

        enum ArgumentType
        {
            noArg = 1,
            requiredArg
        };


        Option(std::string _long_name,
               ArgumentType _type,
               bool _multiple)
               : long_name(_long_name), type(_type), multiple(_multiple)
        {};

        // variable
        std::string long_name;
        ArgumentType type;
        bool multiple;
    };

    // 重载operator<<，输出Option
    std::ostream &operator<<(std::ostream &s, const Option &opt)
    {
        s << "long_name: " << opt.long_name << " "
          << "Argument: " << opt.type << " "
          << "multiple: " << opt.multiple << " ";
        return s;

    };

    class Handler
    {
    public:

        // initialize
        Handler(int argc, char **argv)
                : input(std::vector<std::string>(argv + 1, argv + argc))
        {};

        // Public Methods
        Handler &add_option(std::string long_name,
                            Option::ArgumentType type,
                            bool multiple);

        bool get_option(std::string long_name);

        std::string get_argument(std::string long_name);

        std::vector<std::string> get_arguments(std::string long_name);

        bool is_long(std::string s)
        {
            return ((s.at(0) == '-') && (s.at(1) == '-'));
        };

        void addUsage(std::string line);

        void printUsage();

    private:


        // Properties
        std::vector<Option> declared_options;
        std::vector<std::string> input;
        std::map<std::string, std::vector<std::string> > parsed_input;
        std::vector<std::string> usage;


        // Private Methods
        void update(const Option &option);

        void update_none(const Option &option,
                         std::vector<std::string>::iterator str);

        void update_required(const Option &option,
                             std::vector<std::string>::iterator str);

    };

/**********************************************************
 *
 * @class: Handler
 * @method: add_option
 * @description: 
 * 增加一个新的Option类型，并且在input字符串向量中搜索该类型
 * 并把匹配到的<Option, Argment>类型变量对插入到parsed_input
 * 容器中。
 *
 **********************************************************/

    inline Handler &Handler::add_option(std::string long_name,
                                        Option::ArgumentType type,
                                        bool multiple)
    {

        // Add to declared options
        declared_options.push_back(Option(long_name, type, multiple));
        // Re-assess input
        update(declared_options.back());
        // Chain adding options
        return *this;
    }

/**********************************************************
 *
 * @class: Handler
 * @method: get_option
 * @description: 
 *      Returns whether an option has been passed
 *      from input, given the long name of the option.
 *
 **********************************************************/

    inline bool Handler::get_option(std::string name)
    {
        return ((parsed_input.find(name) != parsed_input.end()) && !parsed_input.empty());
    }

/**********************************************************
 *
 * @class: Handler
 * @method: get_argument
 * @description: Returns the argument passed with a given
 * option.
 *
 **********************************************************/

    inline std::string Handler::get_argument(std::string name)
    {
        if (!get_option(name) || parsed_input.at(name).empty())
            throw std::invalid_argument("option:" + name + " " + "is not found");
        else
            return parsed_input.at(name).front();
    }

/**********************************************************
 *
 * @class: Handler
 * @method: get_argument
 * @description: Returns the vector of arguments passed with
 * a given option.
 *
 **********************************************************/

    inline std::vector<std::string> Handler::get_arguments(std::string name)
    {
        if (!get_option(name))
            throw std::invalid_argument("option:" + name + " " + "is not found");
        else
            return parsed_input.at(name);
    }

/**********************************************************
 *
 * @class: Handler
 * @method: update
 * @description: Given an option, update the parsed argument
 * map appropriately.
 *
 **********************************************************/

    inline void Handler::update(const Option &option)
    {
        // Get start of input
        std::vector<std::string>::iterator str = input.begin();
        while (str != input.end())
        {
            // 在input字符向量中搜索Option
            if (is_long(*str) && (*str).substr(2) == option.long_name)
            {
                // Create empty vector if type = none
                if (option.type == Option::noArg)
                    update_none(option, str);

                if (option.type == Option::requiredArg)
                    update_required(option, str);

            }

            // Next input
            str += 1;
        }
    }

/**********************************************************
 *
 * @class: Handler
 * @method: update_none
 * @description: If the ArgumentType doesn't require an
 * argument, either throw an error (if given an argument)
 * or insert it into the map if it doesn't already exist.
 *
 **********************************************************/

    inline void Handler::update_none(const Option &option,
                                     std::vector<std::string>::iterator str)
    {
        // 如果不是最后一个参数且下一个参数不是一个long option,　就说明是一个argument；
        // 抛出异常
        if (((str + 1) != input.end()) && !is_long(*(str + 1)))
            throw argument_for_none(option.long_name);

        // Only if it doesn't already exist in map
        if (parsed_input.find(option.long_name) == parsed_input.end())
        {
            // Insert empty vector
            parsed_input.insert(std::make_pair(option.long_name, std::vector<std::string>()));
        }
    }

/**********************************************************
 *
 * @class: Handler
 * @method: update_required
 * @description: If the ArgumentType does require an argument,
 * throw an error (if not given an argument) or handle it
 * like an optional argument.
 *
 **********************************************************/

    inline void Handler::update_required(const Option &option,
                                         std::vector<std::string>::iterator str)
    {
        // 如果是最后一个参数，且下一个参数是option，则抛出异常
        if (((str + 1) == input.end()) || is_long(*(str + 1)))
            throw no_argument_for_required(option.long_name);

        // Increment to check the argument
        if (str + 1 != input.end()) str++;
        // If the iterator is not at the end and we haven't encountered another option
        while (str != input.end() && !is_long(*str))
        {
            // If the option doesn't already exist in map
            if (parsed_input.find(option.long_name) == parsed_input.end())
            {
                // Insert into parsed options map
                parsed_input.insert(
                        std::make_pair(option.long_name, std::vector<std::string>(1, *(str)))
                );
            } 
            else
            {
                // If the option allows multiple arguments to it,
                // push into arguments vector
                if (option.multiple)
                    parsed_input.at(option.long_name).push_back(*(str));
                else
                    parsed_input.at(option.long_name)[0] = *str;
            }
            // Next input
            str += 1;
        }
    }

/**********************************************************
 *
 * @class: Handler
 * @method: update_optional
 * @description: If the the option allows multiple arguments
 * passed to an option (Ex. --person Haoran --person Ryan or
 * --person Ryan Haoran Guan) then push arguments into a 
 * vector. Else, the last argument passed will be the 
 * argument set for the option in the map.
 *
 **********************************************************/
    inline void Handler::addUsage(std::string line)
    {
        usage.push_back(line);
    }

    inline void Handler::printUsage()
    {
        for (auto strIt = usage.begin(); strIt != usage.end(); strIt++)
        {
            std::cout << (*strIt) << std::endl;
        }
    }

}

#endif
